require "svglib"

class MoleculeImage
  def initialize(mol_file)
    @atoms, @bonds = parse_mol_file(mol_file)
    @max_x = @atoms.map { |atom| atom.x }.max
    @min_x = @atoms.map { |atom| atom.x }.min
    @max_y = @atoms.map { |atom| atom.y }.max
    @min_y = @atoms.map { |atom| atom.y }.min
  end

  def to_svg(options = {})
    aspect_ratio = ((@max_x - @min_x).abs / (@max_y - @min_y).abs)
    @height  = options[:height]  || 300
    @width   = options[:width]   || @height * aspect_ratio
    @padding = options[:padding] || @height / 10.0

    image = SvgImage.new(:viewBox => "0 0 #{@width} #{@height}") do |canvas|
      canvas.g(:stroke => "black", "stroke-width" => 1, :fill => "none",
               "font-size" => [@height / 16, 13].min, :style => "text-anchor: middle;",
               "font-family" => "Helvetica, Arial, sans-serif", "text-rendering" => :optimizeLegibility) do

        # Draw the bonds between the atoms.
        @bonds.each do |bond|
          atom1, atom2 = @atoms[bond.atom1], @atoms[bond.atom2]
          x1, y1, x2, y2 = transform_x(atom1.x), transform_y(atom1.y),
            transform_x(atom2.x), transform_y(atom2.y)

          case bond.bond_type
          when BOND_SINGLE
            canvas.line(:x1 => x1, :y1 => y1, :x2 => x2, :y2 => y2)
          else # For now, it's the same thing.
            canvas.line(:x1 => x1, :y1 => y1, :x2 => x2, :y2 => y2)
          end
        end

        # Draw the atom symbols.
        @atoms.each do |atom|
          next if atom.symbol == "C" # Carbon atoms aren't usually drawn.
          x, y = transform_x(atom.x), transform_y(atom.y)

          # Draw a white rectangle around the atom symbol. This way the atom
          # symbol and the bond line won't be on top of each other. This is
          # really a big hack, dependant on the SVG rendering model, which
          # states that the elements in an SVG image are rendered in the same
          # order that they are declared. This also means that the image
          # background _must_ be white. Otherwise the boxes will look odd.
          canvas.rect(:x => x - 5, :y => y - 5,
                      :width => 10, :height => 10,
                      :fill => "white", :stroke => "none")
          canvas.text(atom.symbol, :x => x, :y => y, :fill => "black", :stroke => "none",
                      :dy => 4, :dx => 1)
        end
      end
    end
    image.to_svg
  end

  private

  Atom = Struct.new(:x, :y, :symbol)
  Bond = Struct.new(:atom1, :atom2, :bond_type, :bond_stereo)

  # Transforms the x-coordinate from the MOL file to the canvas coordinates.
  def transform_x(x)
    @padding + (x - @min_x) / (@max_x - @min_x).abs * (@width - 2 * @padding)
  end

  # Transforms the y-coordinate from the MOL file to the canvas coordinates.
  def transform_y(y)
    @padding + (y - @min_y) / (@max_y - @min_y).abs * (@height - 2 * @padding)
  end

  # Parses the MOL file. Returns a array containing an array of Atoms
  # and and array of Bonds.
  def parse_mol_file(mol_file)
    lines = mol_file.split(/\r?\n|\r/)
    number_of_atoms = lines[3][0..2].to_i
    number_of_bonds = lines[3][3..5].to_i
    atom_lines = (4 .. 4 + number_of_atoms - 1)
    bond_lines = (4 + number_of_atoms .. 4 + number_of_atoms + number_of_bonds - 1)

    atoms = lines[atom_lines].map do |atom_line|
      Atom.new(atom_line[0..9].to_f, atom_line[10..19].to_f, atom_line[31..33].strip)
    end

    bonds = lines[bond_lines].map do |bond_line|
      Bond.new(bond_line[0..2].to_i - 1, bond_line[3..5].to_i - 1,
               bond_line[6..8].to_i, bond_line[9..11].to_i)
    end

    return atoms, bonds
  end

  # The different bond types.
  BOND_SINGLE   = 1
  BOND_DOUBLE   = 2
  BOND_TRIPLE   = 3
  BOND_AROMATIC = 4
end
